 /*******************************************************************************
  * Copyright (c) 2004, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal.intro.impl.model;

 import java.util.Enumeration ;
 import java.util.Hashtable ;
 import java.util.Vector ;

 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IConfigurationElement;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.ListenerList;
 import org.eclipse.core.runtime.Path;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.Preferences;
 import org.eclipse.core.runtime.SafeRunner;
 import org.eclipse.help.UAContentFilter;
 import org.eclipse.help.internal.UAElementFactory;
 import org.eclipse.jface.util.SafeRunnable;
 import org.eclipse.ui.IPropertyListener;
 import org.eclipse.ui.internal.intro.impl.IntroPlugin;
 import org.eclipse.ui.internal.intro.impl.model.loader.IntroContentParser;
 import org.eclipse.ui.internal.intro.impl.model.loader.ModelLoaderUtil;
 import org.eclipse.ui.internal.intro.impl.model.util.BundleUtil;
 import org.eclipse.ui.internal.intro.impl.model.util.ModelUtil;
 import org.eclipse.ui.internal.intro.impl.util.IntroEvaluationContext;
 import org.eclipse.ui.internal.intro.impl.util.Log;
 import org.eclipse.ui.internal.intro.impl.util.StringUtil;
 import org.eclipse.ui.intro.config.IntroConfigurer;
 import org.osgi.framework.Bundle;
 import org.w3c.dom.Document ;
 import org.w3c.dom.Element ;
 import org.w3c.dom.Node ;

 /**
  * The root class for the OOBE model. It loads the configuration into the
  * appropriate classes.
  *
  * Model rules:
  * <ol>
  * <li>if an attribute is not included in the markup, its value will be null in
  * the model.</li>
  * <li>Resources in plugin.xml are not implicitly resolved against $nl$.
  * Resources in pages are implicitly resolved against $nl$
  * <li>the current page id is set silently when loading the model. You do not
  * need the event notification on model load.</li>
  * <li>Children of a given parent (ie: model root, page, or group) *must* have
  * distinctive IDs otherwise resolving includes and extensions may fail.</li>
  * <li>Containers have the concept of loading children and resolving children.
  * At the model root level, resolving children means resolving ALL extensions of
  * model. At the container level, resolving children means resolving includes.
  * </li>
  * <li>Extensions are resolved before includes at the container level to avoid
  * race conditions. eg: if a page includes a shared group and an extension
  * extends this shared group, you want the include to get the extended group and
  * not the original group.</li>
  * <li>Resolving extensions should not resolve includes. No need to load other
  * models when we dont have to. Plus, extensions can only reference anchors, and
  * so no need to resolve includes.</li>
  * <li>Extensions can not target containers *after* they are resolved. For
  * example, an extension can not target a shared group after it has been
  * included in a page. It can target the initial shared group as a path, but not
  * the group in the page as a path. Again this is because extensions extends
  * anchors that already have a path, not a resolved path.</li>
  * <li>Pages and shared groups that are contributed through extensions become
  * children of the atrget configuration, and so any includes they may have will
  * be resolved correctly.</li>
  * <li>An infinite loop can occur if page A includes from page B and page B in
  * turn includes from page A. ie: cyclic includes. For performnace, accept.
  * </li>
  * <li>When resolving includes, if the target is a container, it must be
  * resolved to resolve its includes correctly. Otherwise, included includes will
  * fail due to reparenting.</li>
  * <li>unresolved includes are left as children of the parent container.</li>
  * <li>Unresolved extensions are left as children of the targetted model.</li>
  * <li>For dynamic awarness, the model is nulled and then reloaded. However, we
  * need to preserve the presentation instance since the UI is already loaded.
  * This is done by reloading the model, and directly resetting the presentation
  * to what it was.</li>
  * <li>Model classes should not have DOM classes as instance vars, and if this
  * is a must, null the DOM class instance the minute you are done. This is
  * because you want the VM to garbage collect the DOM model. Keeping a reference
  * to the DOM model from the Intro model will prevent that.</li>
  * </ol>
  * <li>(since 3.0.2) several passes are used to resolve contributions to
  * anchors that themselves where contributed through an extension. Each time a
  * contribution is resolved, the model tries to resolve all unresolved
  * contribution, recursively.
  * </ul>
  */
 public class IntroModelRoot extends AbstractIntroContainer {

     /**
      * Model constants that fire property change event when they are changed in
      * the model.
      */
     public static final int CURRENT_PAGE_PROPERTY_ID = 1;

     private static final String ATT_CONTENT = "content"; //$NON-NLS-1$
 private static final String ATT_CONFIGURER = "configurer"; //$NON-NLS-1$
 private static final String VAR_THEME = "theme"; //$NON-NLS-1$

     // False if there is no valid contribution to the
 // org.eclipse.ui.into.config extension point. Start off with true, and set
 // to false whenever something bad happens.
 private boolean hasValidConfig = true;
     private boolean isdynamicIntro;
     private IntroConfigurer configurer;
     private IntroTheme theme;
     private IntroPartPresentation introPartPresentation;
     private IntroHomePage homePage;
     private String currentPageId;
     private IntroHomePage standbyPage;

     // the config extensions for this model.
 private IConfigurationElement[] configExtensionElements;

     // maintain listener list for model changes.
 public ListenerList propChangeListeners = new ListenerList();

     // a hashtable to hold all loaded DOMs until resolving all configExtensions
 // is done. Key is one extensionContent DOM element, while value is the
 // IConfigurationElement from where it was loaded. This is needed to extract
 // the base for the xml content file.
 private Hashtable unresolvedConfigExt = new Hashtable ();


     /**
      * Model root. Takes a configElement that represents <config>in the
      * plugin.xml markup AND all the extension contributed to this model through
      * the configExtension point.
      */
     public IntroModelRoot(IConfigurationElement configElement,
             IConfigurationElement[] configExtensionElements) {
         // the config element that represents the correct model root.
 super(configElement);
         this.configExtensionElements = configExtensionElements;

     }

     public void loadModel() {
         getChildren();
     }

     /**
      * Loads the full model. The children of a model root are the presentation,
      * followed by all pages, and all shared groups. Then if the model has
      * extension, its the unresolved container extensions, followed by all
      * extension pages and groups. The presentation is loaded from the
      * IConfiguration element representing the config. All else is loaded from
      * xml content file.
      *
      */
     protected void loadChildren() {
         children = new Vector ();
         if (Log.logInfo)
             Log.info("Creating Intro plugin model...."); //$NON-NLS-1$

         // load presentation first and create the model class for it. If there
 // is more than one presentation, load first one, and log rest.
 IConfigurationElement presentationElement = loadPresentation();
         if (presentationElement == null) {
             // no presentations at all, exit.
 setModelState(true, false, false);
             Log.warning("Could not find presentation element in intro config."); //$NON-NLS-1$
 return;
         }
         
         loadTheme();
         loadConfigurer();

         introPartPresentation = new IntroPartPresentation(presentationElement);
         children.add(introPartPresentation);
         // set parent.
 introPartPresentation.setParent(this);

         // now load all children of the config. There should only be pages and
 // groups here. And order is not important. These elements are loaded
 // from the content file DOM.
 Document document = loadDOM(getCfgElement());
         if (document == null) {
             // we failed to parse the content file. Intro Parser would have
 // logged the fact. Parser would also have checked to see if the
 // content file has the correct root tag.
 setModelState(true, false, false);
             return;
         }

         // set base for this model.
 this.base = getBase(getCfgElement());

         // now load content.
 loadPages(document, getBundle());
         loadSharedGroups(document, getBundle());

         // Attributes of root page decide if we have a static or dynamic case.
 setModelState(true, true, getHomePage().isDynamic());
     }

     /**
      * Sets the presentation to the given presentation. The model always has the
      * presentation as the first child, so use that fact. This method is used
      * for dynamic awarness to enable replacing the new presentation with the
      * existing one after a model refresh.
      *
      * @param presentation
      */
     public void setPresentation(IntroPartPresentation presentation) {
         this.introPartPresentation = presentation;
         presentation.setParent(this);
         children.set(0, presentation);
     }

     /**
      * Resolve contributions into this container's children.
      */
     protected void resolveChildren() {
         // now handle config extension.
 resolveConfigExtensions();
         resolved = true;
     }

     private IConfigurationElement loadPresentation() {
         // If there is more than one presentation, load first one, and log
 // rest.
 IConfigurationElement[] presentationElements = getCfgElement()
             .getChildren(IntroPartPresentation.TAG_PRESENTATION);

         IConfigurationElement presentationElement = ModelLoaderUtil
             .validateSingleContribution(presentationElements,
                 IntroPartPresentation.ATT_HOME_PAGE_ID);
         return presentationElement;
     }
     
     private void loadConfigurer() {
         String cname = getCfgElement().getAttribute(ATT_CONFIGURER);
         if (cname!=null) {
             try {
                 Object obj = getCfgElement().createExecutableExtension(ATT_CONFIGURER);
                 if (obj instanceof IntroConfigurer)
                     configurer = (IntroConfigurer)obj;
             }
             catch (CoreException e) {
                 Log.error("Error loading intro configurer", e); //$NON-NLS-1$
 }
         }
     }
     
     private void loadTheme() {
         Preferences pref = IntroPlugin.getDefault().getPluginPreferences();
         String pid = Platform.getProduct().getId();
         String themeId = pref.getString(pid+"_INTRO_THEME"); //$NON-NLS-1$
 if (themeId.length()==0)
             themeId = pref.getString("INTRO_THEME"); //$NON-NLS-1$
 IConfigurationElement [] elements = Platform.getExtensionRegistry().getConfigurationElementsFor("org.eclipse.ui.intro.configExtension"); //$NON-NLS-1$
 IConfigurationElement themeElement=null;
         for (int i=0; i<elements.length; i++) {
             if (elements[i].getName().equals("theme")) { //$NON-NLS-1$
 String id = elements[i].getAttribute("id"); //$NON-NLS-1$
 if (themeId!=null) {
                     if (id!=null && themeId.equals(id)) {
                         // use this one
 themeElement = elements[i];
                         break;
                     }
                 }
                 else {
                     // see if this one is the default
 String value = elements[i].getAttribute("default"); //$NON-NLS-1$
 if (value!=null && value.equalsIgnoreCase("true")) { //$NON-NLS-1$
 themeElement = elements[i];
                         break;
                     }
                 }
             }
         }
         if (themeElement!=null) {
             theme = new IntroTheme(themeElement);
         }
     }

     /**
      * Loads all pages defined in this config from the xml content file.
      */
     private void loadPages(Document dom, Bundle bundle) {
         String homePageId = getPresentation().getHomePageId();
         String standbyPageId = getPresentation().getStandbyPageId();
         Element[] pages = ModelUtil.getElementsByTagName(dom,
             AbstractIntroPage.TAG_PAGE);
         for (int i = 0; i < pages.length; i++) {
             Element pageElement = pages[i];
             if (pageElement.getAttribute(AbstractIntroIdElement.ATT_ID).equals(
                 homePageId)) {
                 // Create the model class for the Root Page.
 homePage = new IntroHomePage(pageElement, bundle, base);
                 homePage.setParent(this);
                 currentPageId = homePage.getId();
                 children.add(homePage);
             } else if (pageElement.getAttribute(AbstractIntroIdElement.ATT_ID)
                 .equals(standbyPageId)) {
                 // Create the model class for the standby Page.
 standbyPage = new IntroHomePage(pageElement, bundle, base);
                 standbyPage.setParent(this);
                 // signal that it is a standby page.
 standbyPage.setStandbyPage(true);
                 children.add(standbyPage);
             } else {
                 // Create the model class for an intro Page.
 IntroPage page = new IntroPage(pageElement, bundle, base);
                 page.setParent(this);
                 children.add(page);
             }
         }
     }

     /**
      * Loads all shared groups defined in this config, from the DOM.
      */
     private void loadSharedGroups(Document dom, Bundle bundle) {
         Element[] groups = ModelUtil.getElementsByTagName(dom,
             IntroGroup.TAG_GROUP);
         for (int i = 0; i < groups.length; i++) {
             IntroGroup group = new IntroGroup(groups[i], bundle, base);
             group.setParent(this);
             children.add(group);
         }
     }

     /**
      * Handles all the configExtensions to this current model. Resolving
      * configExts means finding target anchor and inserting extension content at
      * target. Also, several passes are used to resolve as many extensions as
      * possible. This allows for resolving nested anchors (ie: anchors to
      * anchors in contributions).
      */
     private void resolveConfigExtensions() {
         for (int i = 0; i < configExtensionElements.length; i++)
             resolveConfigExtension(configExtensionElements[i]);

         // now add all unresolved extensions as model children and log fact.
 Enumeration keys = unresolvedConfigExt.keys();
         while (keys.hasMoreElements()) {
             Element configExtensionElement = (Element) keys.nextElement();
             IConfigurationElement configExtConfigurationElement = (IConfigurationElement) unresolvedConfigExt
                 .get(configExtensionElement);
             Bundle bundle = BundleUtil
                 .getBundleFromConfigurationElement(configExtConfigurationElement);
             String base = getBase(configExtConfigurationElement);
             children.add(new IntroExtensionContent(configExtensionElement,
                 bundle, base, configExtConfigurationElement));

             // INTRO: fix log strings.
 Log
                 .warning("Could not resolve the following configExtension: " //$NON-NLS-1$
 + ModelLoaderUtil.getLogString(bundle,
                             configExtensionElement,
                             IntroExtensionContent.ATT_PATH));
         }
     }

     private void resolveConfigExtension(IConfigurationElement configExtElement) {
         // This call will extract the parent folder if needed.
 Document dom = loadDOM(configExtElement);
         if (dom == null)
             // we failed to parse the content file. Intro Parser would
 // have logged the fact. Parser would also have checked to
 // see if the content file has the correct root tag.
 return;
         resolveConfigExtension(dom, configExtElement);
     }


     private void resolveConfigExtension(Document dom,
             IConfigurationElement configExtElement) {

         // Find the target of this container extension, and add all its
 // children to target. Make sure to pass correct bundle and base to
 // propagate to all children.
 String base = getBase(configExtElement);
         Element extensionContentElement = loadExtensionContent(dom,
             configExtElement, base);
         if (extensionContentElement == null)
             // no extension content defined, ignore extension completely.
 return;

         if (extensionContentElement.hasAttribute("failed")) { //$NON-NLS-1$
 // we failed to resolve this configExtension, because target
 // could not be found or is not an anchor, add the extension to the
 // list of unresolved configExtensions.
 // INTRO: an extensionContent is used as a key, instead of the whole
 // DOM. This is usefull if we need to support multiple extension
 // contents in one file.
 if (!unresolvedConfigExt.containsKey(extensionContentElement))
                 unresolvedConfigExt.put(extensionContentElement,
                     configExtElement);
             return;
         }

         // We resolved a contribution. Now load all pages and shared groups
 // from this config extension. No point adding pages that will never
 // be referenced. Get the bundle from the extensions since they are
 // defined in other plugins.
 Bundle bundle = BundleUtil
             .getBundleFromConfigurationElement(configExtElement);

         Element[] pages = ModelUtil.getElementsByTagName(dom,
             AbstractIntroPage.TAG_PAGE);
         for (int j = 0; j < pages.length; j++) {
             // Create the model class for an intro Page.
 IntroPage page = new IntroPage(pages[j], bundle, base);
             page.setParent(this);
             children.add(page);
         }

         // load all shared groups from all configExtensions to this model.
 loadSharedGroups(dom, bundle);

         // since we resolved a contribution, try resolving some of the
 // unresolved ones before going on.
 unresolvedConfigExt.remove(extensionContentElement);
         tryResolvingExtensions();
     }


     private void tryResolvingExtensions() {
         Enumeration keys = unresolvedConfigExt.keys();
         while (keys.hasMoreElements()) {
             Element extensionContentElement = (Element) keys.nextElement();
             resolveConfigExtension(extensionContentElement.getOwnerDocument(),
                 (IConfigurationElement) unresolvedConfigExt
                     .get(extensionContentElement));
         }
     }


     /**
      * load the extension content of this configExtension into model classes,
      * and insert them at target. A config extension can have only ONE extension
      * content. This is because if the extension fails, we need to be able to
      * not include the page and group contributions as part of the model. If
      * extension content has XHTML content (ie: content attribute is defined) we
      * load extension DOM into target page dom.
      *
      * note: the extension Element is returned to enable creating a child model
      * element on failure.
      *
      * @param
      * @return
      */
     private Element loadExtensionContent(Document dom,
             IConfigurationElement configExtElement, String base) {

         // get the bundle from the extensions since they are defined in
 // other plugins.
 Bundle bundle = BundleUtil
             .getBundleFromConfigurationElement(configExtElement);

         Element[] extensionContents = ModelUtil.getElementsByTagName(dom,
             IntroExtensionContent.TAG_CONTAINER_EXTENSION);
         if (extensionContents.length == 0) {
             extensionContents = ModelUtil.getElementsByTagName(dom,
                     IntroExtensionContent.TAG_CONTAINER_REPLACE);
         }

         // INTRO: change this. we may need to load more than one extension
 // content here.
 // There should only be one container extension. (ver3.0)
 Element extensionContentElement = ModelLoaderUtil
             .validateSingleContribution(bundle, extensionContents,
                 IntroExtensionContent.ATT_PATH);
         if (extensionContentElement == null)
             // no extensionContent defined.
 return null;
         if (UAContentFilter.isFiltered(UAElementFactory.newElement(extensionContentElement), IntroEvaluationContext.getContext())) {
             // whole extension was filtered
 return null;
         }
         
         // Create the model class for extension content.
 IntroExtensionContent extensionContent = new IntroExtensionContent(
             extensionContentElement, bundle, base, configExtElement);
         boolean success = false;
         if (extensionContent.isXHTMLContent())
             success = loadXHTMLExtensionContent(extensionContent);
         else
             success = load3_0ExtensionContent(extensionContent);

         if (success) {
             if (extensionContentElement.hasAttribute("failed")) //$NON-NLS-1$
 extensionContentElement.removeAttribute("failed"); //$NON-NLS-1$
 } else
             extensionContentElement.setAttribute("failed", "true"); //$NON-NLS-1$ //$NON-NLS-2$

         return extensionContentElement;
     }



     /**
      * Insert the extension content into the target.
      *
      * @param extensionContent
      * @return
      */
     private boolean loadXHTMLExtensionContent(
             IntroExtensionContent extensionContent) {
         String path = extensionContent.getPath();
         // path must be pageId/anchorID in the case of anchors in XHTML pages.
 String [] pathSegments = StringUtil.split(path, "/"); //$NON-NLS-1$
 if (pathSegments.length != 2)
             // path does not have correct format.
 return false;
         AbstractIntroPage targetPage = (AbstractIntroPage) findChild(
             pathSegments[0], ABSTRACT_PAGE);
         if (targetPage == null)
             // target could not be found. Signal failure.
 return false;

         // Insert all children of this extension before the target element. Anchors need
 // to stay in DOM, even after all extensions have been resolved, to enable other
 // plugins to contribute. Find the target node.
 Document pageDom = targetPage.getDocument();
         Element targetElement = targetPage.findDomChild(pathSegments[1], "*"); //$NON-NLS-1$
 if (targetElement == null)
             return false;

         // get extension content
 Element[] elements = extensionContent.getElements();
         // insert all children before anchor in page body.
 for (int i = 0; i < elements.length; i++) {
             Node targetNode = pageDom.importNode(elements[i], true);
             // update the src attribute of this node, if defined by w3
 // specs.

             ModelUtil.updateResourceAttributes((Element) targetNode,
                 extensionContent);
             targetElement.getParentNode().insertBefore(targetNode, targetElement);
         }

         if (extensionContent.getExtensionType() == IntroExtensionContent.TYPE_REPLACEMENT) {
             targetElement.getParentNode().removeChild(targetElement);
         }
         
         // now handle style inheritance.
 // Update the parent page styles. skip style if it is null;
 String [] styles = extensionContent.getStyles();
         if (styles != null) {
             for (int i = 0; i < styles.length; i++)
                 ModelUtil.insertStyle(pageDom, styles[i]);
         }

         return true;

     }



     /**
      * Insert the extension content (3.0 format) into the target.
      *
      * @param extensionContent
      * @return
      */
     private boolean load3_0ExtensionContent(IntroExtensionContent extensionContent) {
         String path = extensionContent.getPath();
         int type = extensionContent.getExtensionType();
         AbstractIntroElement target = findTarget(this, path, extensionContent.getId());
         if (target != null && target.isOfType(AbstractIntroElement.ANCHOR) == (type == IntroExtensionContent.TYPE_CONTRIBUTION)) {
             // insert all children of this extension before the target element/anchor.
 insertExtensionChildren(target, extensionContent, extensionContent.getBundle(), extensionContent.getBase());
             // anchors need to stay around to receive other contributions
 if (type == IntroExtensionContent.TYPE_REPLACEMENT) {
                 AbstractIntroContainer parent = (AbstractIntroContainer)target.getParent();
                 parent.removeChild(target);
             }
             handleExtensionStyleInheritence(target, extensionContent);
             return true;
         }
         // appropriate target could not be found. Signal failure.
 return false;
     }

     private void insertExtensionChildren(AbstractIntroElement target,
             IntroExtensionContent extensionContent, Bundle bundle, String base) {
         AbstractIntroContainer parent = (AbstractIntroContainer)target.getParent();
         // insert the elements of the extension before the target
 String mixinStyle = getMixinStyle(extensionContent);
         Element [] children = extensionContent.getChildren();
         parent.insertElementsBefore(children, bundle, base, target, mixinStyle);
     }
     
     private String getMixinStyle(IntroExtensionContent extensionContent) {
         String path = extensionContent.getPath();
         if (!path.endsWith("/@")) //$NON-NLS-1$
 return null;
         String pageId = path.substring(0, path.length()-2);
         IntroModelRoot modelRoot = getModelRoot();
         if (modelRoot==null)
             return null;
         IntroConfigurer configurer = modelRoot.getConfigurer();
         if (configurer==null)
             return null;
         String extensionId = extensionContent.getId();
         // if this is a replace, take the mixin style as what is being replaced
 if (extensionContent.getExtensionType() == IntroExtensionContent.TYPE_REPLACEMENT) {
             IPath ipath = new Path(extensionContent.getPath());
             String s2 = ipath.segment(1);
             if (s2 != null && s2.startsWith("@") && s2.length() > 1) { //$NON-NLS-1$
 extensionId = s2.substring(1);
             }
         }
         return configurer.getMixinStyle(pageId, extensionId);
     }


     /**
      * Updates the inherited styles based on the style attribtes defined in the
      * configExtension. If we are extending a shared group do nothing. For
      * inherited alt-styles, we have to cache the bundle from which we inherited
      * the styles to be able to access resources in that plugin.
      *
      * @param include
      * @param target
      */
     private void handleExtensionStyleInheritence(AbstractIntroElement target,
             IntroExtensionContent extension) {

         AbstractIntroContainer targetContainer = (AbstractIntroContainer)target.getParent();
         if (targetContainer.getType() == AbstractIntroElement.GROUP
                 && targetContainer.getParent().getType() == AbstractIntroElement.MODEL_ROOT)
             // if we are extending a shared group, defined under a config, we
 // can not include styles.
 return;

         // Update the parent page styles. skip style if it is null;
 String [] styles = extension.getStyles();
         if (styles != null)
             targetContainer.getParentPage().addStyles(styles);

         // for alt-style cache bundle for loading resources.
 Hashtable altStyles = extension.getAltStyles();
         if (altStyles != null)
             targetContainer.getParentPage().addAltStyles(altStyles);
     }

     /**
      * Sets the model state based on all the model classes. Dynamic nature of
      * the model is always setto false when we fail to load model for any
      * reason.
      */
     private void setModelState(boolean loaded, boolean hasValidConfig,
             boolean isdynamicIntro) {
         this.loaded = loaded;
         this.hasValidConfig = hasValidConfig;
         this.isdynamicIntro = isdynamicIntro;
     }

     /**
      * Returns true if there is a valid contribution to
      * org.eclipse.ui.intro.config extension point, with a valid Presentation,
      * and pages.
      *
      * @return Returns the hasValidConfig.
      */
     public boolean hasValidConfig() {
         return hasValidConfig;
     }

     /**
      * @return Returns the introPartPresentation.
      */
     public IntroPartPresentation getPresentation() {
         return introPartPresentation;
     }
     
     public IntroConfigurer getConfigurer() {
         return configurer;
     }

     /**
      * @return Returns the rootPage.
      */
     public IntroHomePage getHomePage() {
         return homePage;
     }

     /**
      * @return Returns the standby Page. May return null if standby page is not
      * defined.
      */
     public IntroHomePage getStandbyPage() {
         return standbyPage;
     }

     /**
      * @return all pages *excluding* the Home Page. If all pages are needed,
      * call <code>(AbstractIntroPage[])
      * getChildrenOfType(IntroElement.ABSTRACT_PAGE);</code>
      */
     public IntroPage[] getPages() {
         return (IntroPage[]) getChildrenOfType(AbstractIntroElement.PAGE);
     }

     /**
      * @return Returns the isdynamicIntro.
      */
     public boolean isDynamic() {
         return isdynamicIntro;
     }

     /**
      * @return Returns the currentPageId.
      */
     public String getCurrentPageId() {
         return currentPageId;
     }


     /**
      * Sets the current page. If the model does not have a page with the passed
      * id, the message is logged, and the model retains its old current page.
      *
      * @param currentPageId
      * The currentPageId to set. *
      * @param fireEvent
      * flag to indicate if event notification is needed.
      * @return true if the model has a page with the passed id, false otherwise.
      * If the method fails, the current page remains the same as the
      * last state.
      */
     public boolean setCurrentPageId(String pageId, boolean fireEvent) {
         if (pageId.equals(currentPageId))
             // setting to the same page does nothing. Return true because we did
 // not actually fail. just a no op.
 return true;

         AbstractIntroPage page = (AbstractIntroPage) findChild(pageId,
             ABSTRACT_PAGE);
         if (page == null) {
             // not a page. Test for root page.
 if (!pageId.equals(homePage.getId())) {
                 // not a page nor the home page.
 Log
                     .warning("Could not set current page to Intro page with id: " + pageId); //$NON-NLS-1$
 return false;
             }
         }

         currentPageId = pageId;
         if (fireEvent)
             firePropertyChange(CURRENT_PAGE_PROPERTY_ID);
         return true;
     }

     public boolean setCurrentPageId(String pageId) {
         return setCurrentPageId(pageId, true);
     }

     public void addPropertyListener(IPropertyListener l) {
         propChangeListeners.add(l);
     }

     /**
      * Fires a property changed event. Made public because it can be used to
      * trigger a UI refresh.
      *
      * @param propertyId
      * the id of the property that changed
      */
     public void firePropertyChange(final int propertyId) {
         Object [] array = propChangeListeners.getListeners();
         for (int i = 0; i < array.length; i++) {
             final IPropertyListener l = (IPropertyListener) array[i];
             SafeRunner.run(new SafeRunnable() {

                 public void run() {
                     l.propertyChanged(this, propertyId);
                 }

                 public void handleException(Throwable e) {
                     super.handleException(e);
                     // If an unexpected exception happens, remove it
 // to make sure the workbench keeps running.
 propChangeListeners.remove(l);
                 }
             });
         }
     }

     public void removePropertyListener(IPropertyListener l) {
         propChangeListeners.remove(l);
     }

     /**
      * @return Returns the currentPage. return null if page is not found, or if
      * we are not in a dynamic intro mode.
      */
     public AbstractIntroPage getCurrentPage() {
         if (!isdynamicIntro)
             return null;

         AbstractIntroPage page = (AbstractIntroPage) findChild(currentPageId,
             ABSTRACT_PAGE);
         if (page != null)
             return page;
         // not a page. Test for root page.
 if (currentPageId.equals(homePage.getId()))
             return homePage;
         // return null if page is not found.
 return null;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.internal.intro.impl.model.IntroElement#getType()
      */
     public int getType() {
         return AbstractIntroElement.MODEL_ROOT;
     }


     /**
      * Assumes that the passed config element has a "content" attribute. Reads
      * it and loads a DOM based on that attribute value. It does not explicitly
      * resolve the resource because this method only loads the introContent and
      * the configExt content files. ie: in plugin.xml. <br>
      * This method also sets the base attribute on the root element in the DOM
      * to enable resolving all resources relative to this DOM.
      *
      * @return
      */
     protected Document loadDOM(IConfigurationElement cfgElement) {
         String content = cfgElement.getAttribute(ATT_CONTENT);

         // To support jarring, extract parent folder of where the intro content
 // file is. It is expected that all intro content is in that one parent
 // folder. This works for both content files and configExtension content
 // files.
 Bundle domBundle = BundleUtil
             .getBundleFromConfigurationElement(cfgElement);
         ModelUtil.ensureFileURLsExist(domBundle, content);

         // Resolve.
 content = BundleUtil.getResourceLocation(content, cfgElement);
         Document document = new IntroContentParser(content).getDocument();

         return document;
     }


     private String getBase(IConfigurationElement configElement) {
         String content = configElement.getAttribute(ATT_CONTENT);
         return ModelUtil.getParentFolderToString(content);
     }
     
     public String resolveVariables(String text) {
         if (text==null) return null;
         if (text.indexOf('$')== -1)
             return text;
         // resolve
 boolean inVariable=false;
         StringBuffer buf = new StringBuffer ();
         int vindex=0;
         for (int i=0; i<text.length(); i++) {
             char c = text.charAt(i);
             if (c=='$') {
                 if (!inVariable) {
                     inVariable=true;
                     vindex=i+1;
                     continue;
                 }
                 inVariable=false;
                 String variable=text.substring(vindex, i);
                 String value = getVariableValue(variable);
                 if (value==null)
                     value = "$"+variable+"$"; //$NON-NLS-1$ //$NON-NLS-2$
 buf.append(value);
                 continue;
             }
             else if (!inVariable)
                 buf.append(c);
         }
         return buf.toString();
     }

     private String getVariableValue(String variable) {
         if (variable.equals(VAR_THEME)) {
             if (theme!=null)
                 return theme.getPath();
         }
         if (configurer!=null)
             return configurer.getVariable(variable);
         return null;
     }
     
     public String resolvePath(String extensionId, String path) {
         if (configurer==null) return null;
         return configurer.resolvePath(extensionId, path);
     }

     
     public IntroTheme getTheme() {
         return theme;
     }
 }
